package proxy;

import static java.lang.System.err;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.Socket;
import java.net.SocketException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.MissingResourceException;
import java.util.Set;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;

import org.bouncycastle.openssl.PEMReader;
import org.bouncycastle.openssl.PasswordFinder;
import org.bouncycastle.util.encoders.Base64;

import message.Request;
import message.Response;
import message.request.BuyRequest;
import message.request.CreditsRequest;
import message.request.CryptLoginRequest;
import message.request.DownloadTicketRequest;
import message.request.InfoRequest;
import message.request.ListRequest;
import message.request.LoginRequest;
import message.request.LogoutRequest;
import message.request.PCRequest;
import message.request.UploadRequest;
import message.request.VersionRequest;
import message.response.BuyResponse;
import message.response.CreditsResponse;
import message.response.DownloadTicketResponse;
import message.response.InfoResponse;
import message.response.ListResponse;
import message.response.LoginResponse;
import message.response.MessageResponse;
import message.response.OKLoginResponse;
import message.response.VersionResponse;
import model.DownloadTicket;
import model.FileServerData;
import model.UserSession;
import util.ChecksumUtils;
import util.Config;

public class ClientWorker extends Thread implements IProxy {

	private static int counter = 0;

	private UserSession authenticatedUser;
	private boolean listening;

	private Proxy proxy;
	private Socket socket;

	//private ObjectOutputStream out;
	//private ObjectInputStream in;
	
	private ObjectOutputStream out;
	private ObjectInputStream in;
	private static Cipher rsaencrypt;

	private static Cipher rsadecrypt;
    private static Cipher aesencrypt, aesdecrypt;
    private String keysdir;
    private static String mychallenge;
    private static String username;
    private static String proxyprivatepass;
    private static PrivateKey privateKey = null;
    private static SecretKey seckey = null;
    private static IvParameterSpec iv = null;
	/*
	 * Used to temporary save all versions of a specific file. 
	 * So we can determine the least used server with the highest version of every file.
	 * 
	 * The values get stored in the following manner
	 * HashMap< Filename , HashMap< FileServerPort , Version >>
	 */
	private HashMap<String, HashMap<Integer, Integer>> versionMap;

	private Config messages;
	private Config proxyserver;

	public ClientWorker(Socket socket, Proxy proxy) {
		super("ClientWorkerThread-" + ++counter);

		this.socket = socket;
		this.proxy = proxy;
		messages = new Config("messages");
		authenticatedUser = null;
		proxyprivatepass="12345";
		
		proxyserver = new Config("proxy");
		versionMap = new HashMap<String, HashMap<Integer, Integer>>();
		try
	    {
	      rsaencrypt = Cipher.getInstance("RSA/NONE/OAEPWithSHA256AndMGF1Padding");
	      rsadecrypt = Cipher.getInstance("RSA/NONE/OAEPWithSHA256AndMGF1Padding");
	      aesencrypt = Cipher.getInstance("AES/CTR/NoPadding");
	      aesdecrypt = Cipher.getInstance("AES/CTR/NoPadding");
	      
	      PEMReader pr = new PEMReader(new FileReader(proxyserver.getString("key")), new PasswordFinder(){

	        	@Override
	        	public char[] getPassword() {
	        			
	        		return proxyprivatepass.toCharArray();
					
	        	}
	        });
	      
	      KeyPair keyPair = (KeyPair) pr.readObject();
	      privateKey = keyPair.getPrivate();
	      rsadecrypt.init(Cipher.DECRYPT_MODE, privateKey);
	      
	    }
	    catch(NoSuchAlgorithmException e)
	    {
	      System.out.println("Unable to initialize cipher: " + e.getMessage());
	    }
	    catch(NoSuchPaddingException e)
	    {
	      System.out.println("Unable to initialize cipher: " + e.getMessage());
	    } catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	@Override
	public void run() {
		try {

			out = new ObjectOutputStream(socket.getOutputStream());
			in = new ObjectInputStream(socket.getInputStream());

			listening = true;

			while (listening) {

				try {
					if(authenticatedUser == null)
					{	byte[] bytebuffer = null;
						//in.readFully(bytebuffer);
					bytebuffer = (byte[]) in.readObject();	
					Request r = (Request) decryptrsa(bytebuffer);
						if(r instanceof CryptLoginRequest){
							
							CryptLoginRequest x = (CryptLoginRequest) r;
							login(x);}
					}
					else
					{
					byte[] bytebuffer = null;
					bytebuffer = (byte[]) in.readObject();
					Request request = (Request) decryptaes(bytebuffer);
					
					if (request instanceof CryptLoginRequest) {
						
						out.writeObject(login((CryptLoginRequest) request));
						out.flush();

					} else if (request instanceof LogoutRequest) {
						out.writeObject(encryptaes(logout()));
						out.flush();

					} else if (request instanceof CreditsRequest) {
						
						out.writeObject(encryptaes(credits()));
						out.flush();
					} else if (request instanceof BuyRequest) {

						BuyRequest x = (BuyRequest) request;
						out.writeObject(encryptaes(buy((BuyRequest) x)));
						out.flush();

					} else if (request instanceof ListRequest) {

						out.writeObject(encryptaes(list()));
						out.flush();

					} else if (request instanceof DownloadTicketRequest) {

						DownloadTicketRequest x = (DownloadTicketRequest) request;
						out.writeObject(encryptaes(download((DownloadTicketRequest) x)));
						out.flush();

					} else if (request instanceof UploadRequest) {

						UploadRequest x = (UploadRequest) request;
						out.writeObject(encryptaes(upload((UploadRequest) x)));
						out.flush();

					} else {
						out.writeObject(encryptaes(new MessageResponse(messages.getString("error.unknownCommand"))));
					}
				}

				} catch (ClassNotFoundException e) {
					System.out.println(new MessageResponse(messages
							.getString("error.unexpected")));
					e.printStackTrace();
				} catch (EOFException e) {
					break;
				}
			}

		} catch (SocketException e) {
			// Ignore
		} catch (IOException e) {
			System.out.println(new MessageResponse(messages
					.getString("error.unexpected")));
			e.printStackTrace();
		}
	}

	public void shutdown() {

		listening = false;

		try {
			logout();

			if (socket != null && !socket.isClosed()) {
				socket.shutdownInput();
				socket.shutdownOutput();
				socket.close();
			}

			if (in != null)
				in.close();

			if (out != null)
				out.close();

			socket = null;
			in = null;
			out = null;

		} catch (IOException e) {
			System.out.println(new MessageResponse(messages
					.getString("error.unexpected")));
			e.printStackTrace();
		}
	}

	@Override
	public LoginResponse login(CryptLoginRequest request) throws IOException {

		
		
		
		
		keysdir = proxyserver.getString("keys.dir");
		username=request.getUsername();
		File pemfile = new File(keysdir, request.getUsername() + ".pub.pem");
	      if (!pemfile.isFile())
	      {
	        System.out.println( "No public keyfile for user "+request.getUsername());
	        
	      }
	      if (!pemfile.canRead())
	      {
	        System.out.println("Your public keyfile is not readable for user "+request.getUsername());
	       
	      }
	      try
	      {
	        PEMReader in = new PEMReader(new FileReader(pemfile));
	        PublicKey publicKey = (PublicKey) in.readObject();
	        rsaencrypt.init(Cipher.ENCRYPT_MODE, publicKey);
	        
	      }
	      catch(FileNotFoundException e)
	      {
	        System.out.println( "Your public keyfile is not readable");
	        
	      }
	      catch(IOException e)
	      {
	        System.out.println("While reading users public key");
	        
	      }
	      catch(InvalidKeyException e)
	      {
	        System.out.println( "invalid public key file: " + e.getMessage());
	       
	      }

	      String clichallenge = request.getChallenge();

 //cut here
	      
	      
	      createsecurerandom();
	      
	      byte[] buf = encryptrsa(new OKLoginResponse(clichallenge, mychallenge, seckey.getEncoded(),iv.getIV()));
	      out.writeObject(buf);
	      
	      
	      //out.writeObject(new OKLoginResponse(clichallenge, mychallenge, new String(Base64.encode(seckey.getEncoded())),new String(Base64.encode(iv.getIV()))));
	      //out.flush();

	      
	      PCRequest pcrequest = null;
	     
	    
	      try {
	    byte[] bytebuffer = null;
		bytebuffer = (byte[]) in.readObject();	
		Request r;
		
		r = (Request) decryptaes(bytebuffer);
		if(r instanceof PCRequest){
		pcrequest = (PCRequest) r;
		
		}
		} catch (ClassNotFoundException e) {
			System.out.println("unexpected error");
		}
			
	      
	      
	      if(pcrequest.getProxyChallenge().equals(mychallenge)){ 
		//encrypting end

		try {
			
			authenticatedUser = proxy.authenticate(username);

			if (authenticatedUser != null) {

				// When a second Client wants to log in with a already logged in
				// user!
				if (authenticatedUser.isOnline()) {
					authenticatedUser = null;
					return new LoginResponse(
							LoginResponse.Type.WRONG_CREDENTIALS);
				}

				authenticatedUser.setOnline(true);
				
				//return null;
				return new LoginResponse(LoginResponse.Type.SUCCESS);

			} else {
				return new LoginResponse(LoginResponse.Type.WRONG_CREDENTIALS);
			}

		} catch (MissingResourceException ex) {
			return new LoginResponse(LoginResponse.Type.WRONG_CREDENTIALS);
		}}else{ return new LoginResponse(LoginResponse.Type.WRONG_CREDENTIALS);}
	}

	@Override
	public Response credits() throws IOException {
		
		
			
		if (authenticatedUser != null)
			return new CreditsResponse(authenticatedUser.getCredits());

		return new MessageResponse(messages.getString("error.notAuthenticated"));
	}

	@Override
	public Response buy(BuyRequest credits) throws IOException {

		authenticatedUser.addCredits(credits.getCredits());

		return new BuyResponse(authenticatedUser.getCredits());
	}

	@Override
	public Response list() throws IOException {

		Set<String> allFileNames = new HashSet<String>();

		for (FileServerData fs : proxy.getFileServers())
			allFileNames.addAll(fs.getFileNames());

		return new ListResponse(allFileNames);
	}
	
	public int getHighesVersion(String filename, List<FileServerData> readQuorum) throws IOException {
		int versionMax = 0;
		
		HashMap<Integer, Integer> versions = new HashMap<Integer, Integer>();

		for (FileServerData fs : readQuorum) {
			if (fs.isOnline()) {

				FileserverWorker worker = new FileserverWorker(fs.getAddress(),
						fs.getPort());

				Response fsResponse = worker.version(new VersionRequest(
						filename));

				int version = 0;
				if (fsResponse instanceof VersionResponse)
					version = ((VersionResponse) fsResponse).getVersion();

				versions.put(fs.getPort(), version);
				
				if (version > versionMax)
					versionMax = version;
			}
		}
		
		versionMap.put(filename, versions);	
		return versionMax;
	}

	@Override
	public Response download(DownloadTicketRequest request) throws IOException {

		proxy.calculateQuorum();
		
		String filename = request.getFilename();
		//Getting servers to get highest file version
		List<FileServerData> readQuorum = proxy.getReadQuorum();
		int highestVersion = getHighesVersion(filename, readQuorum);
		
		//if no version found -> file does not exist
		if(highestVersion < 1)
			return new MessageResponse(messages.getString("error.fileDoesNotExist"));
		
		//versionMap got filled with all file-server-versions by getHighesVersion()
		HashMap<Integer, Integer> versions = versionMap.get(filename);
		List<FileServerData> sameVersion = new ArrayList<FileServerData>();
		
		//determine all servers with the newest version
		for(Integer v : versions.keySet()){
			if(versions.get(v) == highestVersion)
				sameVersion.add(proxy.getFileServerMap().get(v));
		}
		
		if(sameVersion.isEmpty())
			return new MessageResponse(messages.getString("error.noFsOnline"));
		
		//sort the servers by usage (asc) and use the first.
		Collections.sort(sameVersion);
		FileServerData leastUsedFs = sameVersion.get(0);
		
		FileserverWorker worker = new FileserverWorker(leastUsedFs.getAddress(), leastUsedFs.getPort());

		Response infoResponseNoType = worker.info(new InfoRequest(filename));
		InfoResponse infoResponse = null;

		if (infoResponseNoType instanceof MessageResponse) {
			return (MessageResponse) infoResponseNoType;
		} else {
			infoResponse = (InfoResponse) infoResponseNoType;
		}
		
		long fileSize = infoResponse.getSize();
		long currentCredits;
		boolean sufficentCredits = false;

		/*
		 * to prevent read write errors.
		 */
		synchronized (authenticatedUser) {
			currentCredits = authenticatedUser.getCredits();

			sufficentCredits = currentCredits >= fileSize;

			if (sufficentCredits)
				authenticatedUser.subtractCredits(fileSize);
		}

		if (!sufficentCredits)
			return new MessageResponse(
					messages.getString("error.insufficentCredits")
							+ " current:" + currentCredits + ", needed:"
							+ fileSize);

		leastUsedFs.addUsage(fileSize);

		DownloadTicket ticket = new DownloadTicket();

		String checksum = ChecksumUtils.generateChecksum(
				authenticatedUser.getUsername(), filename, highestVersion, fileSize);

		ticket.setAddress(leastUsedFs.getAddress());
		ticket.setFilename(filename);
		ticket.setPort(leastUsedFs.getPort());
		ticket.setUsername(authenticatedUser.getUsername());
		ticket.setChecksum(checksum);
		
		this.proxy.getManagementComponent().updateDownloadStatistics(filename);

		return new DownloadTicketResponse(ticket);
	}

	@Override
	public MessageResponse upload(UploadRequest request) throws IOException {
		
		proxy.calculateQuorum();
		
		String content = new String(request.getContent());
		authenticatedUser.addCredits(content.length() * 2);

		String filename = request.getFilename();
		List<FileServerData> readQuorum = proxy.getReadQuorum();
		List<FileServerData> writeQuorum = proxy.getWriteQuorum();
		int replicationCount = 0;
		int versionMax = request.getVersion();

		for (FileServerData fs : readQuorum) {
			if (fs.isOnline()) {
				
				FileserverWorker worker = new FileserverWorker(fs.getAddress(),
						fs.getPort());

				Response fsResponse = worker.version(new VersionRequest(
						filename));

				int version = 0;
				if (fsResponse instanceof VersionResponse)
					version = ((VersionResponse) fsResponse).getVersion();

				if (version > versionMax)
					versionMax = version;
			}
		}
		
		long fileLength = request.getContent().length;
		UploadRequest newRequest = new UploadRequest(filename, versionMax + 1, request.getContent());

		StringBuilder stringBuilder = new StringBuilder();
		
		for (FileServerData fs : writeQuorum) {
			if (fs.isOnline()) {

				FileserverWorker worker = new FileserverWorker(fs.getAddress(),
						fs.getPort());
				
				MessageResponse fsResponse = worker.upload(newRequest);
				
				stringBuilder.append(fsResponse.toString());
				
				if (fsResponse.toString().equals(
						messages.getString("info.upload"))) {
					replicationCount++;
					fs.addFileName(filename);
					fs.addUsage(fileLength);
				}
			}
		}
		if (replicationCount > 0) {
			return new MessageResponse(messages.getString("info.upload")
					+ " ('" + filename + "')");
		} else
			return new MessageResponse(stringBuilder.toString());
					//messages.getString("error.notReplicatedCorrectly"));
	}

	@Override
	public MessageResponse logout() throws IOException {
		if(authenticatedUser != null) {
		
			this.proxy.getManagementComponent().removeCallbackObjects(authenticatedUser.getUsername());
			
			authenticatedUser.setOnline(false);
			authenticatedUser = null;

			return new MessageResponse(messages.getString("info.logout"));
		}
		return new MessageResponse(messages.getString("error.notAuthenticated"));
	}
	
	public static byte[] serialize(Object obj) throws IOException {
        ByteArrayOutputStream b = new ByteArrayOutputStream();
        ObjectOutputStream o = new ObjectOutputStream(b);
        o.writeObject(obj);
        return b.toByteArray();
    }

    public static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
        ByteArrayInputStream b = new ByteArrayInputStream(bytes);
        ObjectInputStream o = new ObjectInputStream(b);
        return o.readObject();
    }

    public static byte[] encryptrsa(Object obj) throws IOException{
		
		 byte[] cryptrequestbyte = serialize(obj);
	     byte[] cryptrequestbyteencoded = null;
	     try {
			cryptrequestbyteencoded = rsaencrypt.doFinal(cryptrequestbyte);
		} catch (IllegalBlockSizeException e1) {
			System.out.println("rsa encoding fehlgeschlagen");
		} catch (BadPaddingException e1) {
			System.out.println("rsa encoding fehlgeschlagen");
		}
	     
	     byte[] cryptrequestbyteencoded64 = Base64.encode(cryptrequestbyteencoded);
		
		
		return cryptrequestbyteencoded64;
		
	}
	
    public static byte[] encryptaes(Object obj) throws IOException{
		
		 byte[] cryptrequestbyte = serialize(obj);
	     byte[] cryptrequestbyteencoded = null;
	     try {
			cryptrequestbyteencoded = aesencrypt.doFinal(cryptrequestbyte);
		} catch (IllegalBlockSizeException e1) {
			System.out.println("aes encoding fehlgeschlagen");
		} catch (BadPaddingException e1) {
			System.out.println("aes encoding fehlgeschlagen");
		}
	     
	     byte[] cryptrequestbyteencoded64 = Base64.encode(cryptrequestbyteencoded);
		
		
		return cryptrequestbyteencoded64;
		
	}

	public static Object decryptrsa(byte[] b) throws IOException, ClassNotFoundException{
		
		byte[] cryptrequestbyteencoded = Base64.decode(b);
		byte[] cryptrequestbyte = null;
		try {
			cryptrequestbyte = rsadecrypt.doFinal(cryptrequestbyteencoded);
		} catch (IllegalBlockSizeException e) {
			System.out.println("rsa decoding fehlgeschlagen");
		} catch (BadPaddingException e) {
			System.out.println("rsa decoding fehlgeschlagen");
		}
		Object obj = deserialize(cryptrequestbyte);
		
		
		return obj;
		
	}
	
public static Object decryptaes(byte[] b) throws IOException, ClassNotFoundException{
		
		byte[] cryptrequestbyteencoded = Base64.decode(b);
		byte[] cryptrequestbyte = null;
		try {
			cryptrequestbyte = aesdecrypt.doFinal(cryptrequestbyteencoded);
		} catch (IllegalBlockSizeException e) {
			System.out.println("aes decoding fehlgeschlagen");
		} catch (BadPaddingException e) {
			System.out.println("aes decoding fehlgeschlagen");
		}
		Object obj = deserialize(cryptrequestbyte);
		
		
		return obj;
		
	}
	
	public static void createsecurerandom()
	{
	      /* generates a 32 byte secure random number */
	      SecureRandom secureRandom = new SecureRandom();
	      byte[] tmp = new byte[32];
	      secureRandom.nextBytes(tmp);
	      mychallenge = new String(Base64.encode(tmp));

	      
	      try
	      {
	        /* generate aes key */
	        KeyGenerator generator = KeyGenerator.getInstance("AES");
	        generator.init(256);
	        seckey = generator.generateKey();

	        /* generate iv */
	        byte[] tmpiv = new byte[16];
	        secureRandom.nextBytes(tmpiv);
	        iv = new IvParameterSpec(tmpiv);

	        /* init aes */
	        aesencrypt.init(Cipher.ENCRYPT_MODE, seckey, iv);
	        aesdecrypt.init(Cipher.DECRYPT_MODE, seckey, iv);
	      } 
	      catch(NoSuchAlgorithmException e)
	      {
	        System.out.println("Error: Unable to generate AES key: " + e.getMessage());
	       
	      }
	      catch(InvalidKeyException e)
	      {
	        System.out.println("Error: invalid AES key: " + e.getMessage());
	        
	      }
	      catch(InvalidAlgorithmParameterException e)
	      {
	        System.out.println("Error: invalid AES parameters: " + e.getMessage());
	        
	      }
	}
}
